#include "lightPool.h"
#include <cuda_runtime_api.h>
#include <iostream>

#define PI 3.14159f

int lp_max(float a, int b) {
	return int(a > b ? a : b);
}

int lp_min(float a, int b) {
	return int(a < b ? a : b);
}

int lp_max(int a, int b) {
	return (a > b ? a : b);
}

int lp_min(int a, int b) {
	return (a < b ? a : b);
}

// Point light version
// Using method described at: http://www.gamasutra.com/view/feature/2942/the_mechanics_of_robust_stencil_.php?page=6
void ds_LightPool::updatePointScissorRects(mat4 *persp, int width, int height, Camera *cam) {
	vec3 camDir = cam->getDirection();
	vec3 camPosition = cam->getPosition();
	mat4 view = cam->getView();
	for(unsigned int i = 0; i < pointLights.size(); ++i) {
		vec3 pos = vec3(view * vec4(pointLights.at(i).position, 1.0f));

		float dist = dot(camDir, pointLights.at(i).position - camPosition);
		// Fast reject if light is far behind
		if(-dist > pointLights.at(i).cutoff_far) {
			pointLightScissorRects[i] = 0;
			pointLightScissorRects[i + pointLights.size()] = 0;
			pointLightScissorRects[i + pointLights.size()*2] = 0;
			pointLightScissorRects[i + pointLights.size()*3] = 0;
			continue;
		}

		float r = pointLights.at(i).cutoff_far;

		int xMin = 0, xMax = width, yMin = 0, yMax = height;

		vec3 pos2 = pos * pos;
		float r2 = r*r;

		float d = (r2*pos2.x - (pos2.x + pos2.z)*(r2 - pos2.z));
		if(d > 0) {
			float nx1, nx2, nz1, nz2;
			float lx2Pluslz2 = pos2.x + pos2.z;
			float rLx = r*pos.x;
			float sqrtD = float(sqrt(d));

			nx1 = (rLx + sqrtD)/lx2Pluslz2;
			nx2 = (rLx - sqrtD)/lx2Pluslz2;
			nz1 = (r - nx1*pos.x)/pos.z;
			nz2 = (r - nx2*pos.x)/pos.z;

			if(!(nx1 == 0 || nx2 == 0 || nz1 == 0 || nz2 == 0)) {

				// < 0 corresponds to points in front?
				float xp;
				vec4 p;
				if(pos.z - r*nz1 < 0) {
					p = *persp * vec4(pos.x - r*nx1, 0, pos.z - r*nz1, 1.0f);
					p /= p.w;
					xp = (.5f*p.x + .5f)*width;
					if(-r*nx1 < 0)
						xMin = lp_max(xp, xMin);
					else
						xMax = lp_min(xp, xMax);
				}

				if(pos.z - r*nz2 < 0) {
					p = *persp * vec4(pos.x - r*nx2, 0, pos.z - r*nz2, 1.0f);
					p /= p.w;
					xp = (.5f*p.x + .5f)*width;
					if(-r*nx2 < 0)
						xMin = lp_max(xp, xMin);
					else
						xMax = lp_min(xp, xMax);
				}
			}
		}

		d = (r2*pos2.y - (pos2.y + pos2.z)*(r2 - pos2.z));
		if(d > 0) {
			float ny1, ny2, nz1, nz2;
			float ly2Pluslz2 = pos2.y + pos2.z;
			float rLy = r*pos.y;
			float sqrtD = float(sqrt(d));

			ny1 = (rLy + sqrtD)/ly2Pluslz2;
			ny2 = (rLy - sqrtD)/ly2Pluslz2;
			nz1 = (r - ny1*pos.y)/pos.z;
			nz2 = (r - ny2*pos.y)/pos.z;

			if(!(ny1 == 0 || ny2 == 0 || nz1 == 0 || nz2 == 0)) {

				// < 0 corresponds to points in front?
				float yp;
				vec4 p;
				if(pos.z - r*ny1 < 0) {
					p = *persp * vec4(0, pos.y - r*ny1, pos.z - r*nz1, 1.0f);
					p /= p.w;
					yp = (.5f*p.y + .5f)*height;
					if(-r*ny1 < 0)
						yMin = lp_max(yp, yMin);
					else
						yMax = lp_min(yp, yMax);
				}

				if(pos.z - r*nz2 < 0) {
					p = *persp * vec4(0, pos.y - r*ny2, pos.z - r*nz2, 1.0f);
					p /= p.w;
					yp = (.5f*p.y + .5f)*height;
					if(-r*ny2 < 0)
						yMin = lp_max(yp, yMin);
					else
						yMax = lp_min(yp, yMax);
				}
			}
		}

		pointLightScissorRects[i] = lp_max(0,xMin);
		pointLightScissorRects[i + pointLights.size()] = lp_max(0,yMin);
		pointLightScissorRects[i + pointLights.size()*2] = lp_min(xMax,width);
		pointLightScissorRects[i + pointLights.size()*3] = lp_min(yMax,height);
	}

	// Copy to cuda
	cudaMemcpy(d_pointLightScissorRects, pointLightScissorRects, sizeof(int)*pointLights.size()*4, cudaMemcpyHostToDevice);
}

size_t spotBufferSize(int numSpotLights) {
	return sizeof(float) * (
		3 + // color
		3 + // direction
		3 + // location
		3)* // half-angle, far cutoff/fade
		numSpotLights;
}

size_t pointBufferSize(int numPointLights) {
	return sizeof(float) * (
		3 +  // color
		3 +  // direction
		2) * // far cutoff/fade
		numPointLights;
}

void ds_LightPool::updatePointLightsCuda() {
	for(unsigned int i = 0; i < pointLights.size(); ++i) {
		pointLightFloats[8*i] = pointLights.at(i).position.x;
		pointLightFloats[8*i + 1] = pointLights.at(i).position.y;
		pointLightFloats[8*i + 2] = pointLights.at(i).position.z;
		pointLightFloats[8*i + 3] = pointLights.at(i).color.x;
		pointLightFloats[8*i + 4] = pointLights.at(i).color.y;
		pointLightFloats[8*i + 5] = pointLights.at(i).color.z;
		pointLightFloats[8*i + 6] = pointLights.at(i).cutoff_far;
		pointLightFloats[8*i + 7] = pointLights.at(i).fade_far;
	}

	cudaMemcpy(d_pointLightFloats, pointLightFloats, pointBufferSize(pointLights.size()), cudaMemcpyHostToDevice);
	numPointLightsUInt = unsigned int(pointLights.size());

	plCudaNeedsUpdate = false;
}

ds_LightPool* ds_LightPool::m_instance = NULL;

ds_LightPool *ds_LightPool::Instance(void) {
	if(!m_instance)
		m_instance = new ds_LightPool();

	return m_instance;
}

void ds_LightPool::Destroy(bool atexit) {
	// If called at exit, this memory has already been freed
	if(m_instance && !atexit) {
		cudaFree(m_instance->d_spotLightFloats);
		m_instance->d_spotLightFloats = NULL;
		cudaFree(m_instance->d_pointLightFloats);
		m_instance->d_pointLightFloats = NULL;
		cudaFree(m_instance->d_pointLightScissorRects);
		m_instance->d_pointLightScissorRects = NULL;
	}

	delete m_instance->pointLightFloats;
	m_instance->pointLightFloats = NULL;
	delete m_instance->pointLightScissorRects;
	m_instance->pointLightScissorRects = NULL;

	delete m_instance;
	m_instance = NULL;
}

ds_LightPool::ds_LightPool() {
	cudaMalloc((void**)&d_spotLightFloats, spotBufferSize(MAX_SPOT_LIGHTS));
	cudaMalloc((void**)&d_pointLightScissorRects, sizeof(unsigned int)*4*MAX_POINT_LIGHTS);

	cudaMalloc((void**)&d_pointLightFloats, pointBufferSize(MAX_POINT_LIGHTS) + sizeof(float)*48);
	unsigned int numLights = 0;
	plCudaNeedsUpdate = false;

	pointLightFloats = new float[pointBufferSize(MAX_POINT_LIGHTS)];
	pointLightScissorRects = new int[sizeof(int)*4*MAX_POINT_LIGHTS];
}

ds_LightPool::~ds_LightPool() {

}

void ds_LightPool::addDirectionalLight(vec3 dir, vec3 color) {
	ds_directional_light l;
	l.color = color;
	l.direction = dir;
	directionalLights.push_back(l);
}

void ds_LightPool::addPointLight(vec3 loc, vec3 color, float fF, float coF) {
	ds_point_light l;
	l.color = color;
	l.position = loc;
	l.cutoff_far = coF;
	if(fF < coF)
		l.fade_far = fF;
	else
		l.fade_far = coF - 1.0f;
	pointLights.push_back(l);
	plCudaNeedsUpdate = true;
}

void ds_LightPool::addSpotLight(vec3 loc, vec3 dir, vec3 color, float ha, float fF, float coF) {
	ds_spot_light l;
	l.color = color;
	l.position = loc;
	l.direction = dir;
	l.cutoff_far = coF;
	if(fF < coF)
		l.fade_far = fF;
	else
		l.fade_far = coF - 1.0f;
	l.half_angle_cos = (float)cos(ha*PI/180.0f);
	spotLights.push_back(l);
}

int ds_LightPool::numDirectionalLightBatches() {
	return directionalLights.size()/DS_LIGHTS_PER_PASS + (directionalLights.size() %  DS_LIGHTS_PER_PASS == 0 ? 0 : 1);
}

int ds_LightPool::numPointLightBatches() {
	return pointLights.size()/DS_LIGHTS_PER_PASS + (pointLights.size() %  DS_LIGHTS_PER_PASS == 0 ? 0 : 1);
}
int ds_LightPool::numSpotLightBatches() {
	return spotLights.size()/DS_LIGHTS_PER_PASS + (spotLights.size() %  DS_LIGHTS_PER_PASS == 0 ? 0 : 1);
}

int ds_LightPool::numSpotLights() {
	return spotLights.size();
}

int ds_LightPool::numPointLights() {
	return pointLights.size();
}

void ds_LightPool::setInversePerspective(mat4 persp) {
	mat4 ip = inverse(persp);
	cudaMemcpy(d_pointLightFloats + 8*MAX_POINT_LIGHTS + 16, &(ip[0][0]), 16*sizeof(float), cudaMemcpyHostToDevice);
}

ds_point_light_device ds_LightPool::pointLightCudaPointers(mat4 *persp, int width, int height, Camera *cam, bool viewChanged) {
	ds_point_light_device dev;
	bool plUpdated = false;

	if(plCudaNeedsUpdate) {
		updatePointLightsCuda();
		plUpdated = true;
	}

	if(viewChanged) {
		updatePointScissorRects(persp, width, height, cam);
		mat4 v = cam->getView();
		mat4 iv = inverse(v);
		cudaMemcpy(d_pointLightFloats + 8*MAX_POINT_LIGHTS, &(iv[0][0]), sizeof(float)*16, cudaMemcpyHostToDevice);
		mat3 itv = transpose(inverse(mat3(iv)));
		cudaMemcpy(d_pointLightFloats + 8*MAX_POINT_LIGHTS + 32, &(itv[0][0]), sizeof(float)*9, cudaMemcpyHostToDevice);
	}
	
	dev.numPointLights = numPointLightsUInt;
	dev.d_pointLightFloats = d_pointLightFloats;
	dev.d_pointLightScissorRects = d_pointLightScissorRects;
	return dev;
}

ds_point_light* ds_LightPool::pointLight(int lightNumber) {
	return &(pointLights.at(lightNumber));
}

ds_spot_light* ds_LightPool::spotLight(int lightNumber) {
	return &(spotLights.at(lightNumber));
}

ds_directional_light_batch ds_LightPool::directionalLightBatch(unsigned int batchNumber) {
	ds_directional_light_batch lb;
	unsigned int numBatches = (unsigned int)numDirectionalLightBatches();
	lb.numLights = DS_LIGHTS_PER_PASS;

	if(batchNumber >= numBatches) {
		cout << "Warning: Requested a directional batch higher than or equal to the number of batches.\n";
		lb.numLights = 0;
		return lb;
	}
	else if(batchNumber == numBatches - 1) {
		lb.numLights = directionalLights.size() - ((numBatches - 1) * DS_LIGHTS_PER_PASS);
	}

	
	unsigned int batchStart = batchNumber * DS_LIGHTS_PER_PASS;
	for(unsigned int i = 0; i < lb.numLights; ++i) {
		lb.color[i] = directionalLights[batchStart + i].color;
		lb.direction[i] = directionalLights[batchStart + i].direction;
	}

	return lb;
}

ds_point_light_batch ds_LightPool::pointLightBatch(unsigned int batchNumber) {
	ds_point_light_batch lb;
	unsigned int numBatches = (unsigned int)numPointLightBatches();
	lb.numLights = DS_LIGHTS_PER_PASS;

	if(batchNumber >= numBatches) {
		cout << "Warning: Requested a point batch higher than or equal to the number of batches.\n";
		lb.numLights = 0;
		return lb;
	}
	else if(batchNumber == numBatches - 1) {
		lb.numLights = pointLights.size() - ((numBatches - 1) * DS_LIGHTS_PER_PASS);
	}

	
	unsigned int batchStart = batchNumber * DS_LIGHTS_PER_PASS;
	for(unsigned int i = 0; i < lb.numLights; ++i) {
		lb.color[i] = pointLights[batchStart + i].color;
		lb.position[i] = pointLights[batchStart + i].position;
		lb.fadeFar[i] = pointLights[batchStart + i].fade_far;
		lb.cutoffFar[i] = pointLights[batchStart + i].cutoff_far;
	}

	return lb;
}

ds_spot_light_batch ds_LightPool::spotLightBatch(unsigned int batchNumber) {
	ds_spot_light_batch lb;
	unsigned int numBatches = (unsigned int)numSpotLightBatches();
	lb.numLights = DS_LIGHTS_PER_PASS;

	if(batchNumber >= numBatches) {
		cout << "Warning: Requested a spot batch higher than or equal to the number of batches.\n";
		lb.numLights = 0;
		return lb;
	}
	else if(batchNumber == numBatches - 1) {
		lb.numLights = spotLights.size() - ((numBatches - 1) * DS_LIGHTS_PER_PASS);
	}

	
	unsigned int batchStart = batchNumber * DS_LIGHTS_PER_PASS;
	for(unsigned int i = 0; i < lb.numLights; ++i) {
		lb.color[i] = spotLights[batchStart + i].color;
		lb.position[i] = spotLights[batchStart + i].position;
		lb.direction[i] = spotLights[batchStart + i].direction;

		lb.cutoff_far[i] = spotLights[batchStart + i].cutoff_far;
		lb.fade_far[i] = spotLights[batchStart + i].fade_far;
		lb.half_angle_cos[i] = spotLights[batchStart + i].half_angle_cos;
	}

	return lb;
}